{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch,gc\n",
    "from torch import nn\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.model_selection import train_test_split\n",
    "from torch.autograd import Variable\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "from tqdm import notebook\n",
    "import matplotlib.pyplot as plt\n",
    "import copy\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 对读入的字符串进行分割处理\n",
    "def data_process(file_name):\n",
    "    data = pd.read_csv(file_name)\n",
    "    quiz = data[\"quizzes\"]\n",
    "    sol = data[\"solutions\"]\n",
    "    x = []\n",
    "    y = []\n",
    "    for i in quiz:\n",
    "        # 将每个题目变为一个9*9*1的list随后转为np array\n",
    "        x.append(np.array([int(j) for j in i]).reshape((9,9,1)))\n",
    "    x = np.array(x)\n",
    "    # normalize\n",
    "    x = x / 9\n",
    "    x = x - 0.5\n",
    "    for i in sol:\n",
    "        # 由于后面返回的标签下标是0-8因此这里把每个真实数据标签-1以匹配下标\n",
    "        y.append(np.array([int(j) for j in i]).reshape((81,1)) - 1)\n",
    "    y = np.array(y)\n",
    "    del(quiz)\n",
    "    del(sol)\n",
    "    # 使用sklearn中分割器进行训练、测试数据分割\n",
    "    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=36)\n",
    "    return x_train, x_test, y_train, y_test"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 读入并处理数据\n",
    "Data = data_process(\"sudoku.csv\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 获取训练和测试数据\n",
    "x_train = Data[0]\n",
    "x_test = Data[1]\n",
    "y_train = Data[2]\n",
    "y_test = Data[3]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 重写dataloader的类方法将自己的数据传入\n",
    "class My_dataset(Dataset):\n",
    "    def __init__(self):\n",
    "        super().__init__()\n",
    "        self.x = x_train\n",
    "        self.y = y_train\n",
    "        self.src,  self.trg = [], []\n",
    "        for i in range(len(x_train)):\n",
    "            self.src.append(self.x[i])\n",
    "            self.trg.append(self.y[i])\n",
    "           \n",
    "    def __getitem__(self, index):\n",
    "        return self.src[index], self.trg[index]\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.src) \n",
    "        \n",
    " # 或者return len(self.trg), src和trg长度一样\n",
    "data_train = My_dataset()\n",
    "# 设置batch_size为800 这里的批量大小是多次尝试后得到的最优值\n",
    "data_loader_train = DataLoader(data_train, batch_size=800, shuffle=False)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 搭建网络\n",
    "use_gpu = torch.cuda.is_available()\n",
    "\n",
    "class Sudoku(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(Sudoku, self).__init__()\n",
    "        self.conv1 = nn.Sequential(\n",
    "            nn.Conv2d(9, 64, kernel_size=(3, 3), stride=(1, 1), padding=(3, 3)),\n",
    "            nn.BatchNorm2d(64),\n",
    "            nn.ReLU(),\n",
    "        )\n",
    "        self.conv2 = nn.Sequential(\n",
    "            nn.Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1)),\n",
    "            nn.BatchNorm2d(64),\n",
    "            nn.ReLU(),\n",
    "        )\n",
    "        self.conv3 = nn.Sequential(\n",
    "            nn.Conv2d(64, 128, kernel_size=(1, 1), stride=(1, 1)),\n",
    "            nn.ReLU(),\n",
    "        )\n",
    "        self.fn1 = nn.Sequential(\n",
    "            nn.Linear(128 * 11 * 3, 81 * 9)\n",
    "        )\n",
    "\n",
    "    def forward(self, x):\n",
    "        x = self.conv1(x)\n",
    "        x = self.conv2(x)\n",
    "        x = self.conv3(x)\n",
    "        # 将其进行reshape\n",
    "        x = x.view(-1,128 * 11 * 3)\n",
    "        x = self.fn1(x)\n",
    "        x = x.reshape(-1, 9)\n",
    "        # x = torch.softmax(x,dim=1) 使用交叉熵损失不能进行softmax会影响其精度\n",
    "        return x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "def train(data_loader,batch_size):\n",
    "    with notebook.tqdm(total=len(data_loader), desc=\"Epoch:{}\".format(epoch + 1), unit='g', leave=False) as pbar:\n",
    "        for i,data in enumerate(data_loader,0):\n",
    "            if use_gpu:\n",
    "                X = data[0].float().cuda()\n",
    "                Y = data[1].cuda()\n",
    "            else:\n",
    "                X = data[0].float()\n",
    "                Y = data[1]\n",
    "               \n",
    "            out = model(Variable(X)\n",
    "            Y = Y.reshape(81*batch_size)\n",
    "            \n",
    "            loss = loss_func(out,Variable(Y.long()))\n",
    "            optimizer.zero_grad()\n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "            # update the bar\n",
    "            pbar.update(1)\n",
    "            #accuracy = torch.max(out.cpu(), 1)[1].numpy() == y_train.cpu().numpy()\n",
    "            accuracy = (torch.max(out,1)[1]==Y).sum()/len(Y)\n",
    "    print('epoch' + str(epoch+1) + ' accuracy:\\t', accuracy.mean())\n",
    "    loss_count.append(loss)\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:1'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch1 accuracy:\t tensor(0.6884)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:2'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch2 accuracy:\t tensor(0.7806)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:3'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch3 accuracy:\t tensor(0.7999)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:4'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch4 accuracy:\t tensor(0.8062)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:5'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch5 accuracy:\t tensor(0.8096)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:6'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch6 accuracy:\t tensor(0.8119)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:7'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch7 accuracy:\t tensor(0.8150)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:8'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch8 accuracy:\t tensor(0.8170)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:9'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch9 accuracy:\t tensor(0.8195)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:10'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch10 accuracy:\t tensor(0.8217)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:11'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch11 accuracy:\t tensor(0.8238)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:12'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch12 accuracy:\t tensor(0.8246)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:13'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch13 accuracy:\t tensor(0.8260)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:14'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch14 accuracy:\t tensor(0.8277)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:15'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch15 accuracy:\t tensor(0.8279)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:16'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch16 accuracy:\t tensor(0.8293)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:17'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch17 accuracy:\t tensor(0.8299)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:18'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch18 accuracy:\t tensor(0.8311)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:19'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch19 accuracy:\t tensor(0.8317)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:20'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch20 accuracy:\t tensor(0.8323)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:21'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch21 accuracy:\t tensor(0.8342)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:22'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch22 accuracy:\t tensor(0.8348)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:23'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch23 accuracy:\t tensor(0.8357)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:24'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch24 accuracy:\t tensor(0.8353)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:25'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch25 accuracy:\t tensor(0.8359)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:26'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch26 accuracy:\t tensor(0.8369)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:27'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch27 accuracy:\t tensor(0.8377)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:28'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch28 accuracy:\t tensor(0.8384)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:29'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch29 accuracy:\t tensor(0.8390)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Epoch:30'), FloatProgress(value=0.0, max=1000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch30 accuracy:\t tensor(0.8395)\n"
     ]
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "#training\n",
    "model = Sudoku()\n",
    "if use_gpu:\n",
    "    model.cuda()\n",
    "# 清理内存\n",
    "gc.collect()\n",
    "torch.cuda.empty_cache()\n",
    "loss_func = torch.nn.CrossEntropyLoss().cuda()\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=0.001)\n",
    "# 可变学习率\n",
    "#scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=6, gamma=0.0004)\n",
    "loss_count = []\n",
    "Epoches = 30\n",
    "for epoch in range(Epoches):\n",
    "    train(data_loader_train,800)\n",
    "    #scheduler.step()\n",
    "torch.save(model.state_dict(),'./sudoku.pth' )\n",
    "plt.plot(range(1,Epoches+1),loss_count,c='r')\n",
    "plt.xlabel('epoch')\n",
    "plt.ylabel('loss')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "class My_dataset2(Dataset):\n",
    "    def __init__(self):\n",
    "        super().__init__()\n",
    "        self.x = x_test\n",
    "        self.y = y_test\n",
    "        \n",
    "        self.src,  self.trg = [], []\n",
    "        for i in range(len(x_test)):\n",
    "            self.src.append(self.x[i])\n",
    "            self.trg.append(self.y[i])\n",
    "           \n",
    "    def __getitem__(self, index):\n",
    "        return self.src[index], self.trg[index]\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.src) \n",
    "        \n",
    " # 或者return len(self.trg), src和trg长度一样\n",
    " \n",
    "data_test2 = My_dataset2()\n",
    "data_loader_test = DataLoader(data_test2, batch_size=100, shuffle=False)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value=''), FloatProgress(value=0.0, max=2000.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "Text(0, 0.5, 'accuracy')"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "acc = []\n",
    "mean_acc=0\n",
    "def test(batch_size):\n",
    "    testnet = Sudoku()\n",
    "    testnet.load_state_dict(torch.load('./sudoku.pth'))\n",
    "    testnet.eval()\n",
    "    testnet.apply(fix_bn)\n",
    "    if use_gpu:\n",
    "        testnet.cuda()\n",
    "    with notebook.tqdm(total=len(data_loader_test), unit='g', leave=False) as pbar:\n",
    "        \n",
    "        for i,data in enumerate(data_loader_test,0):\n",
    "            if use_gpu:\n",
    "                x = data[0].float().cuda()\n",
    "                #print(type(data[0]))\n",
    "                y = data[1].cuda()\n",
    "            else:\n",
    "                x = data[0].float()\n",
    "                \n",
    "                y = data[1]\n",
    "            pre = testnet(Variable(x))\n",
    "            # 更改标签值维度，和网络输出匹配\n",
    "            y = y.reshape(81*batch_size)\n",
    "            pbar.update(1)\n",
    "            accuracy = torch.max(pre.cpu(), 1)[1].numpy() == y.cpu().numpy()\n",
    "            acc.append(accuracy.mean())\n",
    "            #accuracy = (torch.max(pre,1)[1]==y).sum()/len(y)\n",
    "            #print('Batch:{}, Acc:{}'.format(i,accuracy.mean()))\n",
    "# 冻结bn层   \n",
    "def fix_bn(m):\n",
    "    classname = m.__class__.__name__\n",
    "    if classname.find('BatchNorm') != -1:\n",
    "        m.eval()\n",
    "        \n",
    "\n",
    "# 测试集测试\n",
    "test(100)\n",
    "# 计算acc均值并画图\n",
    "mean_acc=np.array(acc).sum()/2000\n",
    "mean_acc=np.ones(2000)*mean_acc\n",
    "b = np.arange(0,2000)\n",
    "plt.plot(b,acc)\n",
    "plt.plot(b,mean_acc,c='r')\n",
    "plt.xlabel('batches')\n",
    "plt.ylabel('accuracy')\n",
    "\n",
    "    \n",
    "    \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Sudoku2(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(Sudoku2, self).__init__()\n",
    "        self.conv1 = nn.Sequential(\n",
    "            nn.Conv2d(9, 64, kernel_size=(3, 3), stride=(1, 1), padding=(3, 3)),\n",
    "            nn.BatchNorm2d(64),\n",
    "            nn.ReLU(),\n",
    "            \n",
    "        )\n",
    "        self.conv2 = nn.Sequential(\n",
    "            nn.Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1)),\n",
    "            nn.BatchNorm2d(64),\n",
    "            nn.ReLU(),\n",
    "            \n",
    "        )\n",
    "        self.conv3 = nn.Sequential(\n",
    "            nn.Conv2d(64, 128, kernel_size=(1, 1), stride=(1, 1)),\n",
    "            nn.ReLU(),\n",
    "        )\n",
    "        self.fn1 = nn.Sequential(\n",
    "            nn.Linear(128 * 11 * 3, 81 * 9)\n",
    "        )\n",
    "\n",
    "    def forward(self, x):\n",
    "        x = self.conv1(x)\n",
    "        x = self.conv2(x)\n",
    "        x = self.conv3(x)\n",
    "        # 将其进行reshape\n",
    "        x = x.view(-1, 128 * 11 * 3)\n",
    "        x = self.fn1(x)\n",
    "        x = x.reshape(-1, 9)\n",
    "        x = torch.softmax(x,dim=1) \n",
    "        return x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用新的预测方式即将数据逐个进行预测"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 用于将数据进行正则化以及解除正则化\n",
    "def denorm(a):\n",
    "    return (a+.5)*9\n",
    "\n",
    "def norm(a):\n",
    "    return (a/9)-.5"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 将模型读入\n",
    "with torch.no_grad():\n",
    "        model = Sudoku2()\n",
    "        model.load_state_dict(torch.load('./sudoku.pth'))\n",
    "        model.eval()\n",
    "# 按照依次填写数字的方式编写函数\n",
    "def inference(sudo):\n",
    "    x = copy.copy(sudo)\n",
    "    while(1):\n",
    "        x = x.reshape((1,9,9,1))\n",
    "        x = torch.from_numpy(x)\n",
    "        out = model(Variable(x.float()))\n",
    "        out = out.detach().numpy()\n",
    "        # pred 为每个位置的预测值\n",
    "        pred = np.argmax(out, axis=1).reshape((9,9))+1\n",
    "        # prob为每个位置的输出预测的概率值\n",
    "        prob = np.around(np.max(out, axis=1).reshape((9,9)), 2)\n",
    "        #prob shape:(81,)\n",
    "        x = denorm(x).reshape((9,9))\n",
    "        x = x.detach().numpy()\n",
    "        mask = (x==0)\n",
    "        if (mask.sum()==0):\n",
    "            break\n",
    "        # prob_new中是未填写部分填写上预测概率值的矩阵\n",
    "        '''此处已填写位置x!=0在mask中出现False，将bool值串与数值相乘后对应位置为0，这样导致当输出概率值为负数是就被卡在了0'''\n",
    "        prob_new = prob*mask\n",
    "        # 此处argmax没有axis参数默认把矩阵展平 选取概率值最大位置\n",
    "        ind = np.argmax(prob_new)\n",
    "        #x y 表示对应的行列，使用整除并取余的方式取出\n",
    "        xx, yy = (ind//9), (ind%9) \n",
    "        val = pred[xx][yy]\n",
    "        x[xx][yy] = val\n",
    "        x = norm(x)\n",
    "    return pred\n",
    "    \n",
    "        \n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "def test_accuracy(feats, labels):\n",
    "    correct = 0\n",
    "    for i,feat in enumerate(feats):\n",
    "        pre = inference(feat)\n",
    "        true = labels[i].reshape((9,9))+1\n",
    "        if(abs(true - pre).sum()==0):\n",
    "            correct += 1\n",
    "    print(correct/feats.shape[0])\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.0\n"
     ]
    }
   ],
   "source": [
    "\n",
    "test_accuracy(x_test[:100], y_test[0:100])\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "def solve_sudoku(game):\n",
    "    \n",
    "    game = game.replace('\\n', '')\n",
    "    game = game.replace(' ', '')\n",
    "    game = np.array([int(j) for j in game]).reshape((9,9,1))\n",
    "    game = norm(game)\n",
    "    game = inference(game)\n",
    "    return game"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "solved puzzle:\n",
      "\n",
      "[[8 8 8 8 8 8 8 8 8]\n",
      " [8 8 8 8 8 8 8 8 8]\n",
      " [8 8 8 8 8 8 8 8 8]\n",
      " [8 8 8 8 8 8 8 5 8]\n",
      " [2 1 1 2 1 2 1 1 6]\n",
      " [2 8 1 3 1 6 8 3 3]\n",
      " [7 4 5 4 4 4 4 4 4]\n",
      " [5 7 5 4 7 7 4 2 2]\n",
      " [4 4 4 4 5 8 8 8 2]]\n"
     ]
    }
   ],
   "source": [
    "game = '''\n",
    "          0 8 0 0 3 2 0 0 1\n",
    "          7 0 3 0 8 0 0 0 2\n",
    "          5 0 0 0 0 7 0 3 0\n",
    "          0 5 0 0 0 1 9 7 0\n",
    "          6 0 0 7 0 9 0 0 8\n",
    "          0 4 7 2 0 0 0 5 0\n",
    "          0 2 0 6 0 0 0 0 9\n",
    "          8 0 0 0 9 0 3 0 5\n",
    "          3 0 0 8 2 0 0 1 0\n",
    "      '''\n",
    "\n",
    "game = solve_sudoku(game)\n",
    "\n",
    "print('solved puzzle:\\n')\n",
    "print(game)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''model = keras.models.Sequential()\n",
    "model.add(BatchNormalization())\n",
    "model.add(Conv2D(64, kernel_size=(3,3), activation='relu', padding='same'))\n",
    "model.add(BatchNormalization())\n",
    "model.add(Conv2D(128, kernel_size=(1,1), activation='relu', padding='same'))\n",
    "model.add(Flatten())\n",
    "model.add(Dense(81*9))\n",
    "model.add(Reshape((-1, 9)))\n",
    "model.add(Activation('softmax'))\n",
    "model.compile(loss='sparse_categorical_crossentropy', optimizer='adam')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
